package org.mtmi.ui.controls.curve;

import java.util.ArrayList;

import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.mtmi.tuio.listeners.DragListener;
import org.mtmi.tuio.listeners.SingleTapListener;
import org.mtmi.tuio.listeners.TUIODragEvent;
import org.mtmi.tuio.listeners.TUIOTapEvent;
import org.mtmi.ui.GraphicUtil;
import org.mtmi.ui.controls.Control;
import org.mtmi.ui.controls.ITouchableControl;
import org.mtmi.ui.controls.TouchControlEvent;
import org.mtmi.ui.layouts.LayoutData;
import org.mtmi.ui.scenes.COLOR;
import org.mtmi.ui.scenes.Scene;

/**
 * This control is a {@link CurveValueTransformer} points editor.
 * display curve in a rectangle with handle points to move points.
 * Can break segment to create new point, or align to successive point to merge them.
 * 
 */
public class EditCurve extends Control 
  implements /*IValueGenerator,*/ ITouchableControl, SingleTapListener, DragListener
{
	public static interface Listener {
		public void pointMoved();
	}
	
    private static final int MG = 10; //margin
    private static final int MG2 = 2*MG; //margin
    private static final int DR = 6; //point's dot radius
    private static final int DD = 2*DR; //point's dot diameter
    private static final int BD = 5; //segment split or merge distance
    
    private static final int S_NONE=0;
    /** moving curve point state, state_data contains point index */
    private static final int S_MOVING_POINT=1;
    /** split segment when drag (state_data contains index of left point of segment) */
    private static final int S_SPLIT_SEG=2;
    
	private int state; //see S_xxx constants above
	private int state_data;  //see state comment
	private int msdx, msdy ;
	
    private CurveValueTransformer curve; 
    private ArrayList<Listener> listeners;
    
	public EditCurve( Scene scene, int _flags ) {
		super( scene, _flags );
		
		setForeground( COLOR.grey70 );
		setBackground( COLOR.grey20 );
		
		state = 0;
		
		setSize( computeSize(null) );
	}
	
	public void addListener( Listener l ) {
		if( listeners==null ) {
			listeners = new ArrayList<Listener>();
		}
		listeners.add( l );
	}
	
	public void removeListener( Listener l ) {
		if( listeners!=null ) {
			listeners.remove( l );
		}
	}
	
	private void notifyPointMoved() {
		if( listeners!=null ) {
			Listener ls[] = listeners.toArray( new Listener[listeners.size()] );
			for( Listener l : ls ) {
				l.pointMoved();
			}
		}
	}
	
	public void setCurve( CurveValueTransformer c ) {
		curve = c;
		redraw();
	}
	
	public CurveValueTransformer getCurve() { return curve; }

	@Override
	public Point computeSize( LayoutData l ) {
		if( l==null ) return new Point( 100, 100 );
		int w = Math.max( l.lw, 100 );
		int h = Math.max( l.lh, 100 );
		return new Point( w, h );
	}
	
	@Override
	public void paint( GC gc ) 
	{
		
		gc.setLineWidth( borderWidth );
		gc.setBackground( getBackground() );
		gc.fillRectangle( location.x, location.y, size.x, size.y );
		gc.setForeground( getForeground() );
		gc.drawRectangle( location.x, location.y, size.x, size.y );
		
		Rectangle rc = getCurveArea();
		gc.setLineWidth( 0 );
		
		//grid:
		gc.setForeground( COLOR.grey0 );
		int N = 8;
		for( int i=0; i<N; i++ ) {
			int x = rc.x+ Math.round( i*rc.width/(float)N );
			gc.drawLine( x, rc.y, x, rc.y+rc.height );
			int y = rc.y+ Math.round( i*rc.height/(float)N );
			gc.drawLine( rc.x, y, rc.x+rc.width, y );
		}
		
		gc.setForeground( COLOR.grey40 );
		gc.drawRectangle( rc );
		int x = rc.x+ rc.width/2;
		gc.drawLine( x, rc.y, x, rc.y+rc.height );
		int y = rc.y+ rc.height/2;
		gc.drawLine( rc.x, y, rc.x+rc.width, y );
		
		if( curve==null ) return ;
		
		ArrayList<FPoint> points = curve.getPoints();
		
		//curve area:
		boolean have_curve = rc.width >=10 && rc.height >=10 ;
		if( have_curve ) {
			
			//draw curve:
			int lx = rc.x;
			int ly = f2y( points.get(0).y(), rc );
			gc.setForeground( COLOR.blue );
			gc.setLineWidth( 2 );
			
			//I wan to show if current moving point will be removed:
			int will_remove = checkToRemoveMovingPoint( false );
			
			for( int i=1; i<points.size(); i++ ) {
				FPoint pt = points.get( i );
				int px = f2x( pt.x(), rc );
				int py = f2y( pt.y(), rc );
				if( will_remove==i || will_remove==i-1 ) {
					gc.setLineWidth( 5 );
				} else {
					gc.setLineWidth( 2 );
				}
				gc.drawLine( lx, ly, px, py );
				lx = px;
				ly = py;
			}
			
			//draw points;
			gc.setLineWidth( 0 );
			gc.setBackground( COLOR.yellow );
			gc.setForeground( COLOR.dark_yellow );
			for( int i=0; i<points.size(); i++ ) {
				FPoint pt = points.get( i );
				int px = f2x( pt.x(), rc );
				int py = f2y( pt.y(), rc );
				gc.fillOval( px-DR, py-DR, DD, DD );
				gc.drawOval( px-DR, py-DR, DD, DD );
				
				if( i==will_remove ) {
					gc.setForeground( COLOR.red );
					gc.setLineWidth( 3 );
					gc.drawOval( px-3*DR, py-3*DR, 3*DD, 3*DD );
					gc.setLineWidth( 0 );
					gc.setForeground( COLOR.dark_yellow );
				}
			}
		}
		
	}

	@Override
	public boolean containsTouch(TouchControlEvent e) {
		return GraphicUtil.rectContains( location, size, e.x, e.y );
	}
	
	private Rectangle getCurveArea() {
		return new Rectangle( location.x+MG, location.y+MG, size.x-MG2, size.y-MG2 );
	}
	/** @return the x pixel for fx value (0..1), rc is the curve area */
	private int f2x( float fx, Rectangle rc ) {
		return Math.round( rc.x+rc.width*fx );
	}
	/** @return the y pixel for fy value (0..1), rc is the curve area */
	private int f2y( float fy, Rectangle rc ) {
		return Math.round( rc.y+rc.height*(1-fy) );
	}
	/** @return the 0..1 value corresponding to x pixel, rc is the curve area */
	private float x2f( int px, Rectangle rc ) {
		return (px-rc.x)/(float)rc.width;
	}
	/** @return the 0..1 value corresponding to y pixel, rc is the curve area */
	private float y2f( int py, Rectangle rc ) {
		return 1-(py-rc.y)/(float)rc.height;
	}

	@Override
	public void tapDown( TUIOTapEvent e ) 
	{
		if( curve==null ) return ;
		ArrayList<FPoint> points = curve.getPoints();
		
		Rectangle rc = getCurveArea();
		boolean have_curve = rc.width >=10 && rc.height>=10 ;
		if( have_curve ) {
			for( int i=points.size()-1; i>=0; i-- ) {
				FPoint pt = points.get(i);
				int px = f2x( pt.x(), rc );
				int py = f2y( pt.y(), rc ); 
				//TODO: yeah really it's oval ...
				if( GraphicUtil.rectContains( px-DR, py-DR, DD, DD, e.ex, e.ey ) ) {
					state = S_MOVING_POINT;
					state_data = i;
					msdx = px-e.ex;
					msdy = py-e.ey;
					break;
				}
			}
			//check for segment:
			if( state==S_NONE && GraphicUtil.rectContains( rc, e.ex, e.ey ) ) {
				int lx = f2x( points.get(0).x(), rc );
				int ly = f2y( points.get(0).y(), rc );
				for( int i=1; i<points.size(); i++ ) {
					FPoint pt = points.get( i );
					int px = f2x( pt.x(), rc );
					int py = f2y( pt.y(), rc );

					if( e.ex >= lx && e.ex <= px ) {
						int d = GraphicUtil.distPointToLine( lx, ly, px, py, e.ex, e.ey );
						if( d <= BD ) {
							state=S_SPLIT_SEG; //split this segment
							state_data = i-1;
							break;
						}
					} 
					lx=px;
					ly=py;
				}
			}
		}
	}

	/** remove the given point if it's aligned on segment (prev,next)
	 * @return the index of removed point, -1 for none */
	private int checkToRemovePoint( int index, boolean doit ) 
	{
		if( curve==null ) return -1;
		ArrayList<FPoint> points = curve.getPoints();
		
		if( points.size()==2 ) return -1; //nothing to remove
		
		FPoint a = points.get( index-1 );
		FPoint m = points.get( index );
		FPoint b = points.get( index+1 );
		
		Rectangle rc = getCurveArea();
		int xa = f2x( a.x(), rc ), ya = f2y( a.y(), rc );
		int xm = f2x( m.x(), rc ), ym = f2y( m.y(), rc );
		int xb = f2x( b.x(), rc ), yb = f2y( b.y(), rc );
		
		int d = GraphicUtil.distPointToLine( xa, ya, xb, yb, xm, ym );
		if( d < BD ) {
			//middle point can be removed:
			if( doit ) {
				points.remove( index );
			}
			return index;
		}
		return -1;
	}
	
	private int checkToRemoveMovingPoint( boolean doit ) {
		if( curve==null ) return -1;
		ArrayList<FPoint> points = curve.getPoints();
		
		if( state!=S_MOVING_POINT ) return -1;
		if( state_data == 0 ) {
			return checkToRemovePoint( 1, doit );
		}
		else if( state_data == points.size()-1 ) {
			return checkToRemovePoint( points.size()-2, doit );
		}
		else {
			return checkToRemovePoint( state_data, doit );
		}
	}
	
	@Override
	public void tapUp( TUIOTapEvent event ) {
		if( curve==null ) return ;
		ArrayList<FPoint> points = curve.getPoints();
		
		//merge the moved point ?
		if( state==S_MOVING_POINT ) {
			if( points.size()>2 ) {
				checkToRemoveMovingPoint( true );
			}
			notifyPointMoved();
		}
		state = S_NONE;
		redraw(); //because of state=2
	}

	@Override
	public void drag( TUIODragEvent e ) {
		if( curve==null ) return ;
		ArrayList<FPoint> points = curve.getPoints();
		if( state==S_MOVING_POINT ) {
			Rectangle rc = getCurveArea();
			
			int npx = e.ex+msdx;
			int npy = e.ey+msdy;
			float nfpx = x2f( npx, rc );
			float nfpy = y2f( npy, rc );
			//constraint on that point ?
			if( state_data == 0 ) {//must be at x=0;
				nfpx=0;
			}
			else if( state_data == points.size()-1 ) { //must be at x=1;
			   nfpx = 1;
			}
			else { //must reside in x range defined by left..right point
				nfpx = Math.max( nfpx, points.get( state_data-1 ).x() );
				nfpx = Math.min( nfpx, points.get( state_data+1 ).x() );
			}
			
			//y constrained in 0..1
			if( nfpy < 0 ) nfpy=0; else if( nfpy > 1 ) nfpy=1f;
			
			FPoint pt = points.get( state_data );
			if( nfpx!=pt.x() || nfpy!=pt.y() ) {
				pt.setCoord( nfpx, nfpy );
				redraw();
			}
		}
		else if( state==S_SPLIT_SEG ) {
			//split the segment:
			Rectangle rc = getCurveArea();
			
			float nx = x2f( e.ex, rc );
			float ny = y2f( e.ey, rc );
			FPoint p = new FPoint( nx, ny );
			points.add( state_data+1, p );
			//now it's a point drag:
			state_data++;
			state=S_MOVING_POINT;
			msdx=0;
			msdy=0;
			redraw();
		}
	}

}
